/*
Copyright (c) 2009, Heinrich C. Spreiter
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

  - Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.
  
  - Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.
  
  - Neither the name of the author nor the names of its contributors may be used
  to endorse or promote products derived from this software without specific
  prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package ch.kynlang.compiler;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.swing.JSplitPane;
import javax.swing.JToolBar;

import org.antlr.runtime.ANTLRInputStream;
import org.antlr.runtime.CharStream;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RuleReturnScope;
import org.antlr.runtime.tree.CommonTreeNodeStream;
import org.antlr.stringtemplate.StringTemplate;
import org.antlr.stringtemplate.StringTemplateGroup;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;

import ch.kynlang.compiler.symbol.ContainerSymbol;
import ch.kynlang.compiler.symbol.Symbol;
import ch.kynlang.compiler.symbol.SymbolTable;
import ch.kynlang.compiler.symbol.UIComponent;
import ch.kynlang.compiler.template.FormatRenderer;
import ch.kynlang.compiler.tree.KynTree;
import ch.kynlang.compiler.tree.KynTreeAdaptor;

/*
 * KynC.java
 *
 * @version 0.1 17.01.2010
 * @author HSP spreiter301@gmail.com
 * 
 * The Compiler!
 */
public class KynC {
	// the symbol table
	private SymbolTable st = null;
	
	// input source
	private InputStream source = null;
	
	// write generated source to this PrintStream
	private PrintStream out = null;
	
	// write errors to this PrintStream
	private PrintStream err = null;
	
	// name of the class
	private String className = null;
	
	public boolean generateMain = false;
	
	public String customInitMethodName = null;
	
	public KynC(String className, InputStream source, PrintStream out, PrintStream err) {
		this.className = className;
		this.source = source;
		this.out = out;
		this.err = err;
	}
	
	/**
	 * tries to compile the input
	 * @return true if successful, false otherwise
	 */
	public boolean compile() {
		st = new SymbolTable();
		try {
			CharStream input = new ANTLRInputStream(source);
			KohYaoNoiLexer lexer = new KohYaoNoiLexer(input);
			CommonTokenStream tokens = new CommonTokenStream(lexer);
			KohYaoNoiParser parser = new KohYaoNoiParser(tokens, st);
			parser.setTreeAdaptor(new KynTreeAdaptor());
			RuleReturnScope r = parser.program();
			KynTree tree = (KynTree) r.getTree();
			
			if(errorcheck()) return false;
			
			CommonTreeNodeStream nodes = new CommonTreeNodeStream(tree);
			DefineSymbols ds = new DefineSymbols(nodes, st);
			ds.downup(tree);
			
			if(errorcheck()) return false;
			
			// evaluate the program
			for(Symbol s : st.rootScope) {
				try {
					s.evaluate();
				} catch(EvaluationException e) {
					st.error(e.toError());
					break;
				}
			}
			
			if(errorcheck()) return false;
			
			if(out == null) return true; // we do not want to generate code
			
			// generate the code
			if(className == null) className = "NoNameDefined";
			
			StringTemplateGroup stg = new StringTemplateGroup(new BufferedReader(
					new InputStreamReader(StringTemplateGroup.class.
							getResourceAsStream("/ch/kynlang/compiler/template/KohYaoNoi.stg"))));
			stg.registerRenderer(String.class, new FormatRenderer());
			stg.registerRenderer(UIComponent.class, new FormatRenderer());
			
			StringTemplate template = null;
			StringBuilder sb = new StringBuilder();
			boolean root = true;
			boolean hasPackage = false;
			for(Symbol s : st.rootScope) {
				if(s.type.getTypeNumber() == KohYaoNoiLexer.PACKAGE) {
					hasPackage = true;
					break;
				}
			}
			if(!hasPackage) {
				// define class
				sb.append("public class " + className + " extends javax.swing.JPanel {\n\n");
				sb.append("///--- <customcode>\n\n");
				sb.append("///--- </customcode>\n\n");
				sb.append("///--- <generatedcode>\n/// DO NOT EDIT CODE IN THIS SECTION\n\n");
			}
			for(Symbol s : st.rootScope) {
				// we don't need all symbols anymore
				switch(s.type.getTypeNumber()) {
					case KohYaoNoiLexer.PACKAGE:
						template = stg.getInstanceOf("package");
						template.setAttribute("symbol", s);
						sb.append(template.toString());
						sb.append("\n\n");
						sb.append("public class " + className + " extends javax.swing.JPanel {\n\n");
						sb.append("///--- <customcode>\n\n");
						sb.append("///--- </customcode>\n\n");
						sb.append("///--- <generatedcode>\n/// DO NOT EDIT CODE IN THIS SECTION\n\n");
						hasPackage = true;
						break;
					case KohYaoNoiLexer.DEF:
						template = stg.getInstanceOf("vardef");
						template.setAttribute("symbol", s);
						sb.append(template.toString());
						sb.append("\n");
						break;
					case KohYaoNoiLexer.WIDGET:
						if(s instanceof ContainerSymbol) {
							ContainerSymbol cs = (ContainerSymbol) s;
							if(root) {
								// initialize
								template = stg.getInstanceOf("init");
								template.setAttribute("symbol", cs);
								template.setAttribute("classname", className);
								template.setAttribute("init", customInitMethodName);
								sb.append(template.toString());
								root = false;
							}
							// special case for JSplitPane
							if(cs.container.equals(JSplitPane.class)) {
								boolean dohorizontal = true;
								if(cs.properties.containsKey("orientation")) {
									dohorizontal = cs.properties.get("orientation").propertyValue.equals("javax.swing.JSplitPane.HORIZONTAL_SPLIT");
								}
								template = stg.getInstanceOf(cs.container.getSimpleName());
								template.setAttribute("symbol", s);
								template.setAttribute("dohorizontal", dohorizontal);
								if(cs.children.size() == 0) {
									template.setAttribute("comp1", null);
									template.setAttribute("comp2", null);
								} else if(cs.children.size() == 1) {
									template.setAttribute("comp1", cs.children.get(0));
									template.setAttribute("comp2", null);
								} else {
									template.setAttribute("comp1", cs.children.get(0));
									template.setAttribute("comp2", cs.children.get(1));
								}
								sb.append(template.toString());
							} else {
								template = stg.getInstanceOf(cs.container.getSimpleName());
								template.setAttribute("symbol", cs);
								sb.append(template.toString());
							}
						} else {
							if(!s.type.getJavaClass().equals(JToolBar.Separator.class)) {
								template = stg.getInstanceOf("UIComponent");
								template.setAttribute("symbol", s);
								sb.append(template.toString());
							}
						}
						sb.append("\n");
						break;
				}
			}
			if(generateMain) {
				template = stg.getInstanceOf("Main");
				template.setAttribute("classname", className);
				sb.append(template.toString());
			}
			sb.append("\n///--- </generatedcode>\n}\n// generated with Koh Yao Noi " + new Date());
			if(out != null) {
				out.println(sb.toString());
			}
			
			return true;
		} catch(Exception e) {
			return false;
		}
	}
	
	private boolean errorcheck() {
		if(st.errors.size() > 0) {
			// found errors
			for(KohYaoNoiError e : st.errors) {
				err.println(e);
			}
			return true;
		}
		return false;
	}
	
	public static void main(String[] args) throws Exception {
		final String tempFilePrefix = "$~0";
		// reset ClassLoader of ClassResolver
		ClassResolver.loader = null;
		
		String workingDirectory = System.getProperty("user.dir");
		String outputFolder = workingDirectory;
		boolean doCompile = false;
		boolean runTest = false;
		String initMethodName = null;
		
		Options options = new Options();
		
		Option help = new Option("h", false, "show this help");
		Option classPath = new Option("cp", true, "set java class path");
		Option initMethod = new Option("i", true, "call this method after panel has been initialized");
		Option outputFolderOpt = new Option("o", true, "set the output folder");
		Option compile = new Option("c", false, "compile to java .class");
		Option test = new Option("t", false, "compile and test this this panel");
		
		// store the class path string
		String cp = ".";
		
		options.addOption(help);
		options.addOption(classPath);
		options.addOption(outputFolderOpt);
		options.addOption(compile);
		options.addOption(test);
		options.addOption(initMethod);
		
		HelpFormatter hf = new HelpFormatter();
		final String usage = "kync [OPTIONS] filename.kyn";
		CommandLineParser parser = new GnuParser();
		try {
			CommandLine cli = parser.parse(options, args);
			if(cli.hasOption("h")) {
				hf.printHelp(usage, options);
				System.exit(0);
			}
			if(cli.hasOption("i")) {
				initMethodName = cli.getOptionValue("i").trim();
			}
			if(cli.hasOption("o")) {
				if(cli.hasOption("c") || cli.hasOption("t")) {
					System.out.println("Ignoring -o " + cli.getOptionValue("o") + ". Can not be used with -c or -t");
				} else {
					outputFolder = cli.getOptionValue("o");
					if(endWithSeparator(outputFolder)) {
						outputFolder = outputFolder.substring(0, outputFolder.length() - 1);
					}
					if(outputFolder == ".") {
						// stay in the working directory
						outputFolder = workingDirectory;
					}
					File dir = new File(outputFolder);
					if(!dir.exists() || !dir.isDirectory()) {
						System.err.println(outputFolder + " is not a folder!");
						System.exit(1);
					}
				}
			}

			// load classes
			if(cli.hasOption("cp")) {
				cp = cp + ";" + cli.getOptionValue("cp");
			}
			String[] paths = cp.split(";");
			List<URL> urls = new ArrayList<URL>();
			for(String path : paths) {
				File f = new File(path);
				if(f.exists()) {
					urls.add(f.toURI().toURL());
				} else {
					System.err.println("Classpath '" + path + "' not found");
					System.exit(1);
				}
			}
			
			// setup the class loader
			URL[] aURLs = new URL[urls.size()];
			for(int i = 0; i < urls.size(); i++) {
				aURLs[i] = urls.get(i);
			}
			ClassResolver.loader = new URLClassLoader(aURLs);
			
			if(cli.hasOption("c")) {
				doCompile = true;
			}
			if(cli.hasOption("t")) {
				runTest = true;
			}
			
		} catch(ParseException pe) {
			hf.printHelp(usage, options);
			System.exit(1);
		}
		if(args.length == 0) {
			System.err.println("No input file.\nUse kync -h for help");
			System.exit(1);
		}
		

		String inputFileName = args[args.length-1];
		if(!inputFileName.endsWith(".kyn")) {
			System.err.println("No input file.\nUse kync -h for help");
			System.exit(1);
		}
		
		File inputFile = new File(inputFileName);
		if(!inputFile.exists()) {
			System.err.println("No input file.\nUse kync -h for help");
			System.exit(1);
		}
		
		// get the filename without any path
		StringBuilder sb = new StringBuilder();
		
		int strLen = inputFileName.length();
		int pos = strLen - 1;
		while(pos >= 0) {
			char c = inputFileName.charAt(pos);
			if(c == '\\' || c == '/') {
				break;
			} else {
				sb.insert(0, c);
			}
			pos--;
		}
		
		String className = sb.toString();
		String pathToFile = inputFileName.substring(0, inputFileName.length() - className.length());
		className = className.substring(0, className.indexOf("."));
		
		InputStream source = new BufferedInputStream(new FileInputStream(inputFileName));
		
		File outputFile = new File(pathToFile + tempFilePrefix + className + ".java");
		if(!outputFile.exists()) {
			outputFile.createNewFile();
		}
		
		PrintStream out = new PrintStream(outputFile);
		KynC compiler = new KynC(className, source, out, System.err);
		compiler.generateMain = runTest;
		compiler.customInitMethodName = initMethodName;
		boolean success = compiler.compile();
		out.close();
		
		if(!success) {
			outputFile.delete();
			System.exit(1);
		} else {
			File o = new File(pathToFile + className + ".java");
			StringBuffer customCode = new StringBuffer();
			StringBuffer imports = new StringBuffer();
			if(o.exists()) {
				// search for
				// ///--- <customcode>
				// and store all content until
				// ///--- </customcode>
				BufferedReader codeReader = new BufferedReader(new FileReader(o));
				boolean foundStart = false;
				String line = null;
				while((line = codeReader.readLine()) != null) {
					if(line.trim().startsWith("import ")) {
						imports.append(line + "\n");
					} else if(line.trim().startsWith("///--- <customcode>")) {
						foundStart = true;
						break;
					}
				}
				
				// add all the code to the buffer until we reach ///--- </customcode>
				while(foundStart && (line = codeReader.readLine()) != null) {
					if(line.trim().startsWith("///--- </customcode>")) {
						break;
					}
					customCode.append(line + "\n");
				}	
				codeReader.close();
				o.delete();
			}
			
			// add customCode and imports if there are any
			if(customCode.length() > 0 || imports.length() > 0) {
				BufferedReader outputFileReader = new BufferedReader(new FileReader(outputFile));
				BufferedWriter outputFileWriter = new BufferedWriter(new FileWriter(o));
				String line = null;
				while((line = outputFileReader.readLine()) != null) {
					if(line.trim().startsWith("public class "))  {
						// append all the import
						outputFileWriter.append(imports.toString());
					}
					outputFileWriter.append(line + "\n");
					if(line.trim().startsWith("///--- <customcode>"))  {
						// append all the code
						String append = customCode.toString();
						outputFileWriter.append(append.substring(0, append.length() -1)); // remove last \n
					}
				}
				outputFileWriter.close();
				outputFileReader.close();
				outputFile.delete();
			} else {
				outputFile.renameTo(o);
			}
			
			if(doCompile || runTest) {
				// compile
				Runtime rt = Runtime.getRuntime();
				String javaFile = pathToFile + className + ".java";
				Process p = rt.exec("javac " + ((cp == null) ? "-cp ." : "-cp " + cp + ";.") + " " + javaFile);
				p.waitFor(); // wait for javac
				
				if(p.exitValue() != 0) {
					// error!
					System.err.println("Can not compile " + className + ".java");
					InputStream err = p.getErrorStream();
					byte[] buffer = new byte[512];
					while(err.read(buffer) != - 1) {
						StringBuilder msg = new StringBuilder();
						for(byte b : buffer) {
							msg.append((char) b);
						}
						System.err.print(msg.toString());
					}
					System.exit(1);
				} else if (runTest) {
					rt.exec("java " + ((cp == null) ? "-cp ." : "-cp " + cp + ";.") + " " + javaFile.substring(0, javaFile.length() - 5).replace("/", ".").replace("\\", "."));
				}
			}
			
			System.exit(0);
		}

	}
	
	public static boolean endWithSeparator(String s) {
		return s.endsWith("/") || s.endsWith("\\");
	}
	
	public static void showHelpAndExit() {
		StringTemplateGroup group =  new StringTemplateGroup(new BufferedReader(
				new InputStreamReader(StringTemplateGroup.class.
						getResourceAsStream("/ch/kynlang/compiler/template/help.stg"))));
		StringTemplate template = group.getInstanceOf("help");
		System.out.println(template.toString());
		System.exit(0);
	}
}
